0%

QEMU 是如何工作的?

Quick EMUlator(QEMU)

简介

  • 仿真器和虚拟化器
  • 模式:
    • 用户模式仿真:允许一个(Linux)进程执行在不同架构的CPU上,该模式下,QEMU 可以作为进程级虚拟机。
    • 系统模式仿真:允许仿真完整的系统,包括处理器和配套的外设,该模式下,QEMU 也可以作为系统虚拟机
  • 主要用途
    • 跨平台编译与开发环境
    • 虚拟化,尤其是外设仿真,作为对 xen 和 kvm 的补充
    • 安卓仿真器(作为 SDK 的一部分)

动态二进制翻译

动态转译过程:

  1. 读取源指令流(二进制)-> gen_intermediate_code()
  2. 解释为微操作(TCG 微操作)-> tcg_gen_code()
  3. 编译为目标文件(二进制)
  4. 动态产生目标指令流
  • 功能转译:模仿源处理器完成的功能而非执行过程
  • 动态二进制翻译:像一个不含解释器的 JIT 编译器
    • 解释器逐一执行指令,使得执行速度因为固定的额外开销显著变慢
    • 作为替代,QEMU 用自己需要的方式转换代码
      • 翻译基本块 -> 生成主机目标代码
      • 在翻译缓存中存储翻译块
  • Tiny Code Generator (TCG)
    • 将源二进制流转换为微操作
    • (固定的)寄存器映射来减少产生的 load/store 指令
    • 翻译块:不包含跳转指令,以分支指令为分隔,翻译块被转换为主机指令的一个单一序列,并放入翻译缓存
      • 缓存中的翻译块用它们的客户机虚拟地址(如PC值)索引,因此可以快速查得
      • 翻译缓存的大小默认为 32MB,但是可以自行配置
      • 一旦缓存运行超出空间,全部缓存被清理
    • 块链接
  • 在主循环中,通过 cpu_exec() 进入下一代码块
  • 程序执行至无后续链接的代码块,则通过后置代码返回 cpu_exec()

块链接

  • 正常情况下,每个转译块的执行,被特殊代码块的执行环绕
    • 前置代码初始化处理器以生成跳转并执行到指定代码块的主机代码
    • 后置代码恢复处理器至正常状态并返回到主循环
  • 在每个代码块后返回主循环显著增加了额外开销
    • 因此在下一个代码块已经翻译好的情况下,QEMU会patch原始块,以跳过后置代码而直接执行下一代码块。
  • 在代码块间直接跳转:
    • 在存放返回至后置代码的指令和代码块返回间,留出跳转空间
    • 每当一个块返回时,尝试链接至下一代码块
  • 当链接在数个连续块间完成后,代码块间将形成链和环
    • 这允许 QEMU 仿真内循环时,无需运行额外的代码
    • 这也意味着,在循环过程中,只有在遇到未链接或未翻译的块需要被执行时,才将控制权还给 QEMU
  • 异步中断
    • QEMU 不主动检查每个基础块中是否有硬件中断在等待,而是由用户异步的调用特定函数 cpu_interrupt() 来告知 QEMU 中断正在等待
    • 该函数的功能是重置当前正在执行的块链,返回至 CPU 仿真器的主循环
    • 异步中断常常来自其它线程

寄存器映射

  • 当目标架构寄存器个数多于源架构寄存器个数
  • 如果目标架构寄存器不够,则采用每个代码块/每个trace/每个循环为单位进行寄存器映射
  • 最差情况,放弃映射不常用的源架构寄存器
  • 处理 PC 寄存器
    • TPC(目标) 和 SPC(源)不同
    • 对间接跳转指令,保存 SPC 的寄存器必须提供将 SPC 映射至 TPC 的方式,即仿真器在任何时刻都需要跟踪 SPC

其它主要 QEMU 组件

  • 内存地址转译
    • 软件控制内存管理单元(模型)来翻译目标虚拟地址为主机虚拟地址:两级客户机物理页描述符表
    • 映射客户机虚拟地址和主机虚拟地址:定位翻译缓存(tlb_table),直接将客户机虚拟地址转换为主机虚拟地址
    • 映射客户机虚拟地址和寄存器输入输出功能:缓存被使用的内存映射 I/O 访问(iotlb)
  • 设备仿真
    • i440FX host PCI bridge, Cirrus CLGD 5446 PCI VGA card, PS/2 mouse & keyboard, PCI IDE interfaces (HDD, CDROM), PCI & ISA network adapters, Serial ports, PCI UHCI USB controller & virtual USB hub, …

SoftMMU

  • MMU 虚拟->物理地址转换在每个访存操作中都要完成
    • 通过地址转换缓存加速转换
    • 为了避免每次 MMU 映射改变造成翻译块缓存清空,QEMU 采用物理方式索引转译缓存
      • 每个基础块由其物理地址索引
      • 当 MMU 映射改变时,仅仅基础块的链需要改变(一个基础块可能不再能直接跳转到下一个)

补充

  • QEMU 架构
    QEMU 架构

  • QEMU 存储栈
    QEMU 存储栈

  • QEMU I/O 控制流
    QEMU I/O 控制流

QEMU 与 KVM 交互

QEMU 纯软件虚拟化模拟器,几乎可以模拟任何硬件设备。通常用来模拟一台能够独立运行操作系统的虚拟机.QEMU 模拟硬件将指令转译给真正的硬件。

纯软件实现仿真器性能较低,往往需要 KVM (硬件虚拟化加速)帮助完成高频的 CPU 和内存虚拟化,QEMU 则负责 I/O 虚拟化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 第一步,获取到 KVM 句柄
kvmfd = open("/dev/kvm", O_RDWR);
// 第二步,创建虚拟机,获取到虚拟机句柄。
vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
// 第三步,为虚拟机映射内存,还有其他的 PCI,信号处理的初始化。ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem);
// 第四步,将虚拟机镜像映射到内存,相当于物理机的 boot 过程,把镜像映射到内存。
// 第五步,创建 vCPU,并为 vCPU 分配内存空间。
ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid);
vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
// 第五步,创建 vCPU 个数的线程并运行虚拟机。ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
// 第六步,线程进入循环,并捕获虚拟机退出原因,做相应的处理。
for (;;) {
ioctl(KVM_RUN)
switch (exit_reason) {
case KVM_EXIT_IO: /* ... */
case KVM_EXIT_HLT: /* ... */
}
}
// 这里的退出并不一定是虚拟机关机,
// 虚拟机如果遇到 I/O 操作,访问硬件设备,缺页中断等都会退出执行,
// 退出执行可以理解为将 CPU 执行上下文返回到 Qemu。

QEMU 源码结构

  • /vl.c: 最主要的模拟循环,虚拟机环境初始化,和 CPU 的执行。
  • /target-arch/translate.c: 将 guest 代码翻译成不同架构的 TCG 操作码。
  • /tcg/tcg.c: 主要的 TCG 代码。
  • /tcg/arch/tcg-target.c: 将 TCG 代码转化生成主机代码。
  • /cpu-exec.c: 主要寻找下一个二进制翻译代码块,如果没有找到就请求得到下一个代码块,并且操作生成的代码块。
函数 路径 注释
main_loop /vl.c 判断运行状态
qemu_main_loop_start /cpus.c 分时运行 CPU 核
struct CPUState /target-xyz/cpu.h CPU 状态结构体
cpu_exec /cpu_exec.c 主要执行循环
struct TranslationBlock /exec-all.h TB(二进制翻译代码块)结构体
cpu_gen_code translate-all.c 初始化真正的代码生成
tcg_gen_code /tcg/tcg.c tcg 代码翻译成 host 代码

参考链接

  1. QEMU Internals Slides
  2. 一文读懂QEMU模拟器
  3. QEMU Tech
  4. QEMU Internals